package client;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.nio.file.*;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Scanner;
import java.util.concurrent.Semaphore;

import javax.swing.JDialog;
import javax.swing.JOptionPane;

import clientUI.LogonDialog;

import static java.nio.file.StandardWatchEventKinds.*;

import shared.*;
import static java.nio.file.StandardOpenOption.*;

public class Client
{
    public Semaphore myfilesLock;
    public User currentUser;
    public LocalEventHandler localHandler;
    public ShareHandler shareHandler;
    private MasterServerInterface masterServer;

    private Thread eventHandlerThread;

    public Client()
    {
	// this.connectToMasterServer();
	myfilesLock = new Semaphore(1);
    }

    public void init() throws Exception
    {
	System.out.println("Reading configuration file");
	Configuration.readConfigurationFile();
	// check configuration
	System.out.println("Checking Configuration...");
	if (Files.notExists(Configuration.getHomePath()))
	{
	    Files.createDirectories(Configuration.getHomePath());
	}
	;
	if (!Files.isDirectory(Configuration.getHomePath()))
	{
	    throw new Exception(
		    "The home sync path in configuration is not a directory");
	}
	;

	System.out.println("Restoring file information from disk...");
	this.restoreFromDisk();

	System.out.println("Starting local event handler...");
	this.localHandler = new LocalEventHandler(this);
	localHandler.detectEvents();
	this.eventHandlerThread = new Thread(localHandler);
	eventHandlerThread.start();

    }

    // get remote reference to Master Server
    public void connectToMasterServer()
    {
	Registry registry = null;
	boolean allFail = true;
	while (allFail)
	{
	    try
	    {
		if (allFail == true
			&& InetAddress.getByName(Constants.MASTER_SERVER_1_IP)
				.isReachable(Constants.IP_WAIT_TIME))
		{
		    try
		    {

			registry = LocateRegistry
				.getRegistry(Constants.MASTER_SERVER_1_IP);
			this.masterServer = (MasterServerInterface) registry
				.lookup(Constants.MASTER_SERVER_NAME);
			allFail = false;

		    } catch (Exception e1)
		    {
			System.out
				.println("Failed to connect to master server 1, now trying master server 2!");
		    }
		}
	    } catch (Exception eee)
	    {
	    }

	    try
	    {
		if (allFail == true
			&& InetAddress.getByName(Constants.MASTER_SERVER_2_IP)
				.isReachable(Constants.IP_WAIT_TIME))
		{
		    try
		    {
			registry = LocateRegistry
				.getRegistry(Constants.MASTER_SERVER_2_IP);
			this.masterServer = (MasterServerInterface) registry
				.lookup(Constants.MASTER_SERVER_NAME);
			allFail = false;

		    } catch (Exception e2)
		    {
			System.out
				.println("Failed to connect to master server 2, now trying master server 3!");
		    }
		}
	    } catch (Exception eee)
	    {
	    }

	    try
	    {
		if (allFail == true
			&& InetAddress.getByName(Constants.MASTER_SERVER_3_IP)
				.isReachable(Constants.IP_WAIT_TIME))
		{
		    try
		    {

			registry = LocateRegistry
				.getRegistry(Constants.MASTER_SERVER_3_IP);
			this.masterServer = (MasterServerInterface) registry
				.lookup(Constants.MASTER_SERVER_NAME);

		    } catch (Exception e3)
		    {
			System.out
				.println("All master servers are down! Now wait..");
		    }
		}
	    } catch (Exception eee)
	    {
	    }
	}

	System.out.println("Successfully connected to master server!!");

    }

    /**
     * Four Cases: 01: file not in server list but in local list 10: file in
     * server list but not in local list 11: file both in localList and server
     * list
     * 
     */
    public void syncWithServer()
    {

	User serverUser = null;
	try
	{
	    serverUser = masterServer.getUser(currentUser.getUsername());
	} catch (RemoteException e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    return;
	}

	localHandler.detectEvents();
	HashMap<String, char[]> occurMap = Toolkit.getInstance().mergeKey(
		serverUser.myFiles, currentUser.myFiles);

	for (String file : occurMap.keySet())
	{
	    String occurence = new String(occurMap.get(file));

	    if (occurence.equals("10"))
	    {
		handleNewRemote(serverUser.myFiles, file);
	    } else if (occurence.equals("01"))
	    {
		handleNewLocal(serverUser.myFiles, file);
	    }

	    else if (occurence.equals("11"))
	    {
		handleMerge(serverUser.myFiles, file);
	    } else
	    {
		System.err.println("Fatal Error: undefined condition");
	    }
	}
    }

    public User signUp(User user) throws RemoteException
    {
	// String password = user.getPassword();
	// password = Utilities.computeHashFromBytes(password.getBytes());
	// user.setPassword(password);
	return masterServer.signUp(user);
    }

    public void logon(String username, String password) throws RemoteException
    {
	// password = Utilities.computeHashFromBytes(password.getBytes());
	User user = masterServer.logOn(username, password);
	user.myFiles = getCurrentList();
	currentUser = user;
    }

    public void close()
    {
	this.saveToDisk();
    }

    // in remote server but not local
    private void handleNewRemote(HashMap<String, FileDescriptor> serverList,
	    String file)
    {

	System.out.println("handleNewRemote" + file);
	FileDescriptor serverFile = serverList.get(file);

	if (serverFile.isDirectory())
	{
	    Path absolutePath = Configuration.getHomePath().resolve(file);
	    try
	    {
		Files.createDirectories(absolutePath);
	    } catch (IOException e)
	    {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	    try
	    {
		this.myfilesLock.acquire();
	    } catch (InterruptedException e)
	    {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	    currentUser.myFiles.put(file, serverFile);
	    this.myfilesLock.release();
	    return;
	}

	// is regular file
	try
	{
	    serverFile = masterServer.getLatestFileDescriptor(serverFile
		    .getFileID());
	    readRemote(serverFile);

	} catch (Exception e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    return;
	}

	try
	{
	    this.myfilesLock.acquire();
	} catch (InterruptedException e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}
	currentUser.myFiles.put(serverFile.getPathname(), serverFile);
	this.myfilesLock.release();
    }

    // not in server but in local
    private void handleNewLocal(HashMap<String, FileDescriptor> serverList,
	    String file)
    {
	System.out.println("handleNewLocal" + file);
	FileDescriptor localFile = currentUser.myFiles.get(file);
	Path originalPath = Configuration.getHomePath().resolve(
		localFile.getPathname());
	if (localFile.isDeletedLocal())
	{
	    try
	    {
		this.myfilesLock.acquire();
	    } catch (InterruptedException e)
	    {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	    currentUser.myFiles.remove(file);
	    this.myfilesLock.release();
	    return;
	}
	try
	{
	    if (localFile.getFileID() == -1)
	    {
		// newly created file
		createRemote(localFile);
	    } else
	    {
		// server deleted the file
		if (localFile.isModifiedLocal() && !localFile.isDirectory())
		{
		    // conflicting make new copy locally
		    String conflictPathName = "(conflict copy of "
			    + currentUser.getUsername() + ")" + "_version_"
			    + localFile.getVersion()
			    + originalPath.getFileName();

		    Path conflictPath = originalPath.getParent().resolve(
			    conflictPathName);

		    Files.copy(originalPath, conflictPath,
			    StandardCopyOption.REPLACE_EXISTING);
		}
		Files.deleteIfExists(originalPath);

		try
		{
		    this.myfilesLock.acquire();
		} catch (InterruptedException e)
		{
		    // TODO Auto-generated catch block
		    e.printStackTrace();
		}
		currentUser.myFiles.remove(file);
		this.myfilesLock.release();

	    }
	} catch (Exception e)
	{
	    e.printStackTrace();
	}
    }

    // in both need to merge
    @SuppressWarnings("finally")
    private void handleMerge(HashMap<String, FileDescriptor> serverList,
	    String file)
    {
	System.out.println("handleMerge: " + file);
	FileDescriptor localFile = currentUser.myFiles.get(file);
	FileDescriptor serverFile = serverList.get(file);
	if (localFile.isDeletedLocal())
	{
	    try
	    {
		deleteRemote(serverFile);
	    } finally
	    {
		return;
	    }
	}

	// TODO sharing file case

	assert (serverFile.isDirectory() == localFile.isDirectory());
	if (serverFile.isDirectory())
	{
	    return;
	}

	try
	{
	    if (localFile.getFileID() == -1)
	    {

		if (localFile.getHash().equals(serverFile.getHash()))
		{
		    serverFile.setDeletedLocal(false);
		    serverFile.setModifiedLocal(false);
		    currentUser.myFiles.put(serverFile.getPathname(),
			    serverFile);
		} else
		{
		    readRemote(serverFile);
		}
		return;

	    }

	    if (localFile.isModifiedLocal())
	    {

		serverFile = masterServer.getLatestFileDescriptor(serverFile
			.getFileID());

		if (localFile.getHash().equals(serverFile.getHash()))
		{
		    localFile.setModifiedLocal(false);
		    return;
		}

		if (localFile.getVersion() == serverFile.getVersion())
		{
		    writeRemote(localFile);
		} else if (localFile.getVersion() < serverFile.getVersion())
		{

		    Path originalPath = Configuration.getHomePath().resolve(
			    localFile.getPathname());
		    String conflictPathName = "(conflict copy of "
			    + currentUser.getUsername() + ")" + "_version_"
			    + localFile.getVersion()
			    + originalPath.getFileName();

		    Path conflictPath = originalPath.getParent().resolve(
			    conflictPathName);
		    Files.copy(originalPath, conflictPath,
			    StandardCopyOption.REPLACE_EXISTING);
		    readRemote(serverFile);
		}

		else
		{
		    System.err
			    .println("Weird!! Local Verison number larger than server version");
		}

	    } else
	    {

		// update only when local version is not as server version
		if (localFile.getVersion() != serverFile.getVersion())
		{
		    readRemote(serverFile);
		}
	    }
	} catch (Exception e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    return;
	}
    }

    private void readRemote(FileDescriptor fd) throws RemoteException,
	    InterruptedException
    {

	fd = masterServer.getLatestFileDescriptor(fd.getFileID());
	SeekableByteChannel channel = null;
	System.out.println("reading server file " + fd.getPathname()
		+ " version " + fd.getVersion());

	Path path = Configuration.getHomePath().resolve(fd.getPathname());
	boolean isOk = false;
	System.out.println(fd.getDataServers().size());
	for (DataServerState ds : fd.getDataServers())
	{
	    try
	    {
		if (Files.notExists(path))
		{
		    Files.createDirectories(path.getParent());
		    Files.createFile(path);
		} else
		{
		    Files.delete(path);
		    Files.createFile(path);
		}
		channel = Files.newByteChannel(path, WRITE);
		int offset = 0;
		while (true)
		{

		    DataChunk dataChunk = ds.getRemoteDataServer().readData(
			    fd.getFileID(), offset);
		    assert (dataChunk.getOffset() == offset);
		    offset += dataChunk.getContent().length;
		    ByteBuffer writeBuffer = ByteBuffer.wrap(dataChunk
			    .getContent());
		    int bytesWritten = channel.write(writeBuffer);
		    if (bytesWritten != dataChunk.getContent().length)
			throw new CloudException("Incomplete write! Failure!");
		    if (dataChunk.isLast())
		    {
			System.out.println("offset: "
				+ dataChunk.getContent().length);
			break;
		    }

		}

		channel.close();
		isOk = true;
		break;
	    } catch (Exception e)
	    {
		e.printStackTrace();
		continue;
	    }
	}
	try
	{
	    channel.close();
	} catch (IOException e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}

	if (!isOk)
	{
	    throw new CloudException("fail to read " + fd.getPathname()
		    + " from server!");
	}

	localHandler.detectEvents();
	this.myfilesLock.acquire();
	// update current file list
	// unset isModified by successfully written file
	// recompute hash
	fd = masterServer.getLatestFileDescriptor(fd.getFileID());
	fd.setModifiedLocal(false);
	fd.setHash(Utilities.computeHash(path.toString()));
	currentUser.myFiles.put(fd.getPathname(), fd);
	this.myfilesLock.release();
	System.out.println("File: " + fd.getPathname() + " version "
		+ fd.getVersion() + " is now updated from server");
    }

    private void writeRemote(FileDescriptor fd) throws Exception
    {
	// request

	FileDescriptor newFd = masterServer.requestModify(fd);
	System.out.println("writing local file " + fd.getPathname()
		+ " version " + fd.getVersion());
	Boolean[] isAllOk = { false };
	ArrayList<Thread> threads = new ArrayList<Thread>();
	for (DataServerState ds : newFd.getDataServers())
	{
	    FileWriterRemote fileWriter = new FileWriterRemote(newFd, ds,
		    isAllOk);
	    Thread writerThread = new Thread(fileWriter);
	    threads.add(writerThread);
	    writerThread.start();
	}

	// join thread
	for (Thread t : threads)
	{
	    t.join();
	}

	if (!isAllOk[0])
	{
	    throw new CloudException("None of the write to dataserver succed");
	}

	// update current file list
	// unset isModified by successfully written file
	Path path = Configuration.getHomePath().resolve(fd.getPathname());
	this.myfilesLock.acquire();
	fd.setHash(Utilities.computeHash(path.toString()));
	fd.setModifiedLocal(false);
	fd.setVersion(fd.getVersion() + 1);
	currentUser.myFiles.put(fd.getPathname(), fd);
	this.myfilesLock.release();

	System.out.println("File: " + fd.getPathname() + " version "
		+ fd.getVersion() + " pushed to server!");
    }

    private void createRemote(FileDescriptor fd) throws Exception
    {
	System.out.println("creating server file " + fd.getPathname()
		+ " version " + fd.getVersion());

	this.myfilesLock.acquire();
	fd.getOwners().add(currentUser.getUsername());
	fd = masterServer.requestCreate(fd);
	this.myfilesLock.release();

	System.out.println("file " + fd.getPathname() + " version "
		+ fd.getVersion() + " is created on server machine");
	if (fd.isDirectory())
	{
	    return;
	}
	System.out.println("writing local file " + fd.getPathname()
		+ " version " + fd.getVersion());
	Boolean[] isAllOk = { false };
	ArrayList<Thread> threads = new ArrayList<Thread>();
	for (DataServerState ds : fd.getDataServers())
	{
	    FileWriterRemote fileWriter = new FileWriterRemote(fd, ds, isAllOk);
	    Thread writerThread = new Thread(fileWriter);
	    threads.add(writerThread);
	    writerThread.start();
	}

	// join thread
	for (Thread t : threads)
	{
	    t.join();
	}

	if (!isAllOk[0])
	{
	    throw new CloudException("None of the write to dataserver succed");
	}

	// update current file list
	// unset isModified by successfully written file
	Path path = Configuration.getHomePath().resolve(fd.getPathname());
	this.myfilesLock.acquire();
	fd.setHash(Utilities.computeHash(path.toString()));
	fd.setModifiedLocal(false);
	fd.setVersion(fd.getVersion() + 1);
	currentUser.myFiles.put(fd.getPathname(), fd);
	this.myfilesLock.release();

	System.out.println("File: " + fd.getPathname() + " version "
		+ fd.getVersion() + " is written on server!");
    }

    private void deleteRemote(FileDescriptor fd) throws Exception
    {
	System.out.println("deleting server file " + fd.getPathname()
		+ " version " + fd.getVersion());
	masterServer.requestDelete(fd.getFileID());
	this.myfilesLock.acquire();
	currentUser.myFiles.remove(fd.getPathname());
	this.myfilesLock.release();
	System.out.println("File: " + fd.getPathname() + " version "
		+ fd.getVersion() + " is deleted from server");
    }

    class FileWriterRemote implements Runnable
    {
	private FileDescriptor fd;
	private Boolean[] isAllOK;
	private DataServerState dataServer;

	FileWriterRemote(FileDescriptor fd, DataServerState dataServer,
		Boolean[] isAllOk2)
	{
	    this.fd = fd;
	    this.isAllOK = isAllOk2;
	    this.dataServer = dataServer;
	}

	// TODO
	@Override
	public void run()
	{
	    String filename = fd.getPathname();
	    Path file = Configuration.getHomePath().resolve(filename);
	    try
	    {
		SeekableByteChannel channel = Files.newByteChannel(file);
		channel.position(0);
		int num;

		while (true)
		{
		    ByteBuffer buffer = ByteBuffer
			    .wrap(new byte[Constants.DATA_CHUNK_SIZE]);

		    int offset = (int) channel.position();
		    num = channel.read(buffer);

		    byte[] content;
		    DataChunk chunk;
		    if (num < 0)
		    {
			content = new byte[0];
			chunk = new DataChunk(content, offset, true);
			dataServer.getRemoteDataServer().writeData(fd, chunk);
			break;
		    }

		    content = Arrays.copyOf(buffer.array(), num);
		    chunk = new DataChunk(content, offset, false);
		    dataServer.getRemoteDataServer().writeData(fd, chunk);
		}
		channel.close();

	    } catch (Exception e)
	    {
		e.printStackTrace();
		System.err.print(e.getMessage());
	    }

	    this.isAllOK[0] = true;
	}

    }

    class FolderWatcherHandler implements Runnable
    {

	@Override
	public void run()
	{
	    WatchService folderWatcher = Configuration.getFolderWatcher();
	    while (true)
	    {
		WatchKey key = null;
		try
		{
		    key = folderWatcher.poll();
		} catch (Exception x)
		{
		    System.err.println("File watcher failed!");
		}

		if (key == null)
		{
		    try
		    {
			// In unit of millisecond
			Thread.sleep(3 * 1000);
		    } catch (InterruptedException e)
		    {
		    }
		} else
		{
		    for (WatchEvent<?> event : key.pollEvents())
		    {
			WatchEvent.Kind<?> kind = event.kind();
			if (event == OVERFLOW)
			    continue;

			@SuppressWarnings("unchecked")
			WatchEvent<Path> ev = (WatchEvent<Path>) event;
			Path file = ev.context();
			file = file.toAbsolutePath();
			Path homePath = Configuration.getHomePath();
			String relativePathname = homePath.relativize(file)
				.toString();

			if (kind == ENTRY_CREATE)
			{
			    // TODO implement the actions to take
			    System.out.println("Create" + relativePathname
				    + file.toString());

			} else if (kind == ENTRY_MODIFY)
			{
			    System.out.println("Modify" + relativePathname
				    + file.toString());
			} else if (kind == ENTRY_DELETE)
			{
			    System.out.println("DELETE" + relativePathname
				    + file.toString());
			}
		    }

		    boolean valid = key.reset();
		    if (!valid)
			break;
		}
	    }
	}
    }

    public void ping() throws RemoteException
    {
	this.masterServer.hello();
    }

    public void test() throws Exception
    {
	User newUser = new User();
	newUser.setUsername("mohanz");
	newUser.setPassword("jiji");

	FileDescriptor ds = new FileDescriptor();
	ds.setPathname("TTT.txt");
	ds.setDirectory(false);
	ds.setOwners(new ArrayList<String>());
	ds.getOwners().add("mohanz");
	//
	// try {
	// this.masterServer.signUp(newUser);
	// ds = this.masterServer.requestCreate(ds);
	// byte[] bytes = new byte[10];
	// for (int i = 0; i < bytes.length; i++)
	// bytes[i] = 1;
	//
	// ds.getDataServers().get(0).getRemoteDataServer()
	// .writeData(ds, new DataChunk(bytes, 0, true));
	// for (int i = 0; i < bytes.length; i++)
	// bytes[i] = 2;
	// ds.getDataServers().get(0).getRemoteDataServer()
	// .writeData(ds, new DataChunk(bytes, 0, true));
	//
	// } catch (RemoteException e) {
	// // TODO Auto-generated catch block
	// e.printStackTrace();
	// }
	// System.out.println(ds.getFileID());
	// System.out.println(ds.getDataServers().get(0).getHostName());

	// String s;
	//
	// this.restoreFromDisk();
	// s = this.currentUser.myFiles.toString();
	// System.out.println("What is read:");
	// System.out.println(s);
	//
	// this.currentUser.myFiles.put("Test", ds);
	// System.out.println("Modifying");
	// this.currentUser.myFiles.get("Test").setPathname("Changed");
	// this.saveToDisk();
	// System.out.println("Saved");
	//
	// this.restoreFromDisk();
	// s = this.currentUser.myFiles.toString();
	// System.out.println("What is read:");
	// System.out.println(s);

    }

    @SuppressWarnings("unchecked")
    private void restoreFromDisk()
    {
	this.currentUser = new User();
	this.currentUser.myFiles = new HashMap<String, FileDescriptor>();

	try
	{
	    FileInputStream fis = new FileInputStream(
		    Configuration.STORAGE_PATH);
	    ObjectInputStream storage = new ObjectInputStream(fis);
	    this.currentUser.myFiles = (HashMap<String, FileDescriptor>) storage
		    .readObject();

	} catch (Exception e)
	{
	    // TODO Auto-generated catch block
	    System.err.println(e.getMessage());
	}
    }

    private void saveToDisk()
    {
	ObjectOutputStream storage = null;
	Path file = Paths.get(Configuration.STORAGE_PATH);
	try
	{
	    OutputStream fos = Files.newOutputStream(file, CREATE);
	    storage = new ObjectOutputStream(fos);
	    this.myfilesLock.acquire();
	    storage.writeObject(this.currentUser.myFiles);
	    this.myfilesLock.release();
	    storage.close();
	} catch (Exception e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	}

    }

    public MasterServerInterface getMasterServer()
    {
	return masterServer;
    }

    public HashMap<String, FileDescriptor> getCurrentList()
    {
	this.localHandler.detectEvents();
	return currentUser.myFiles;
    }

    public static void main(String[] args)
    {

	Client me = new Client();

	try
	{
	    me.init();
	} catch (Exception e)
	{
	    // TODO Auto-generated catch block
	    e.printStackTrace();
	    System.err.println("Couldn't initialize the client "
		    + e.getLocalizedMessage());
	    System.exit(0);
	}

	me.connectToMasterServer();

	LogonDialog logonWindow = new LogonDialog(me);
	logonWindow.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
	logonWindow.setVisible(true);
	while (!logonWindow.isOk)
	{
	    try
	    {
		Thread.sleep(1000);
	    } catch (InterruptedException e)
	    {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	}

	// starting synchronize with server
	Thread syncThread = new Thread(new Synchronizer(me));
	syncThread.start();

	ShareHandler s = new ShareHandler(me);
	me.shareHandler = s;
	Thread shareHandleThread = new Thread(s);
	shareHandleThread.start();

    }

}
